﻿#region File and License Information
/*
<File>
	<License Type="BSD">
		Copyright © 2009 - 2012, Outcoder. All rights reserved.
	
		This file is part of Calcium (http://calciumsdk.net).

		Redistribution and use in source and binary forms, with or without
		modification, are permitted provided that the following conditions are met:
			* Redistributions of source code must retain the above copyright
			  notice, this list of conditions and the following disclaimer.
			* Redistributions in binary form must reproduce the above copyright
			  notice, this list of conditions and the following disclaimer in the
			  documentation and/or other materials provided with the distribution.
			* Neither the name of the <organization> nor the
			  names of its contributors may be used to endorse or promote products
			  derived from this software without specific prior written permission.

		THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
		ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
		WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
		DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
		DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
		(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
		LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
		ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
		(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
		SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
	</License>
	<Owner Name="Daniel Vaughan" Email="danielvaughan@outcoder.com"/>
	<CreationDate>2011-12-15 15:27:25Z</CreationDate>
</File>
*/
#endregion

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;

namespace Outcoder.UI.Xaml
{
	public class UICommand<TParameter> : DelegateCommand<TParameter>, IUICommand
	{
		public string Id { get; set; }

		/// <summary>
		/// Creates a UICommand that will perform its canExecute evaluation 
		/// using the async model.
		/// </summary>
		/// <param name="id">An arbitrary identifier that is used 
		/// to identify commands in your application during execution. 
		/// Can be <c>null</c>.</param>
		/// <param name="executeAction">The action that is performed 
		/// when the execute method is called. This is a required parameter.</param>
		/// <param name="canExecuteFunc">Can be <c>null</c>. A method that is used to determine 
		/// if this command can be executed. 
		/// Ordinarily this will set the visual state of a button, for example, to be disabled.
		/// If <c>null</c> the command is set to always executable.</param>
		public UICommand(
			string id,
			Func<TParameter, Task> executeAction,
			Func<TParameter, Task<bool>> canExecuteFunc = null, 
			IExceptionHandler exceptionHandler = null,
			[CallerFilePath] string filePath = null,
			[CallerLineNumber] int lineNumber = 0)
			: base(executeAction, false, canExecuteFunc, exceptionHandler, filePath, lineNumber)
		{
			Id = id;
		}

		/// <summary>
		/// Creates a UICommand that will perform its canExecute evaluation 
		/// using the non-async model present in the <see cref="DelegateCommand"/>.
		/// </summary>
		public UICommand(
			Action<TParameter> executeAction,
			Func<TParameter, bool> canExecuteFunc = null, 
			string id = null,
			IExceptionHandler exceptionHandler = null,
			[CallerFilePath] string filePath = null,
			[CallerLineNumber] int lineNumber = 0)
			: base(executeAction, canExecuteFunc, exceptionHandler, filePath, lineNumber)
		{
			Id = id;
		}
		
		string text;

		/// <summary>
		/// The title of this command that may be displayed to the user.
		/// </summary>
		public string Text
		{
			get
			{
				return text;
			}
			set
			{
				AssignPropertyValue(ref text, value, false);
			}
		}

		Func<TParameter, Task<string>> iconUrlFunc;

		/// <summary>
		/// A func to retrieve the icon URL of the command.
		/// </summary>
		public Func<TParameter, Task<string>> IconUrlFunc
		{
			get
			{
				return iconUrlFunc;
			}
			set
			{
				AssignPropertyValue(ref iconUrlFunc, value, true);
			}
		}

		string iconUrl;

		public string IconUrl
		{
			get
			{
				return iconUrl;
			}
			set
			{
				AssignPropertyValue(ref iconUrl, value, false);
			}
		}

		async Task RefreshIconUrlAsync(TParameter parameter)
		{
			if (iconUrlFunc != null)
			{
				IconUrl = await iconUrlFunc(parameter);
			}
		}

		#region Icon Font

		Func<TParameter, Task<string>> iconFontFunc;

		/// <summary>
		/// A func to retrieve the icon URL of the command.
		/// </summary>
		public Func<TParameter, Task<string>> IconFontFunc
		{
			get
			{
				return iconFontFunc;
			}
			set
			{
				AssignPropertyValue(ref iconFontFunc, value, true);
			}
		}

		string iconFont;

		public string IconFont
		{
			get
			{
				return iconFont;
			}
			set
			{
				AssignPropertyValue(ref iconFont, value, false);
			}
		}

		async Task RefreshIconFontAsync(TParameter parameter)
		{
			if (iconFontFunc != null)
			{
				IconFont = await iconFontFunc(parameter);
			}
		}

		#endregion

		Func<TParameter, Task<string>> textFunc;

		/// <summary>
		/// A func to retrieve the text of the command.
		/// </summary>
		public Func<TParameter, Task<string>> TextFunc
		{
			get
			{
				return textFunc;
			}
			set
			{
				AssignPropertyValue(ref textFunc, value, true);
			}
		}

		Func<TParameter, Task<bool>> isVisibleFunc;

		public Func<TParameter, Task<bool>> IsVisibleFunc
		{
			get
			{
				return isVisibleFunc;
			}
			set
			{
				AssignPropertyValue(ref isVisibleFunc, value, true);
			}
		}

		bool visible = true;

		public bool Visible
		{
			get
			{
				return visible;
			}
			set
			{
				AssignPropertyValue(ref visible, value, false);
			}
		}

		#region IsChecked
		Func<TParameter, Task<bool>> isCheckedFunc;

		public Func<TParameter, Task<bool>> IsCheckedFunc
		{
			get
			{
				return isCheckedFunc;
			}
			set
			{
				AssignPropertyValue(ref isCheckedFunc, value, true);
			}
		}

		async Task RefreshIsCheckedAsync(TParameter parameter)
		{
			if (isCheckedFunc != null)
			{
				IsChecked = await isCheckedFunc(parameter);
			}
		}

		bool isChecked;

		public bool IsChecked
		{
			get
			{
				return isChecked;
			}
			set
			{
				AssignPropertyValue(ref isChecked, value, false);
			}
		}
		#endregion

		#region IconCharacter
		Func<TParameter, Task<string>> iconCharacterFunc;

		public Func<TParameter, Task<string>> IconCharacterFunc
		{
			get
			{
				return iconCharacterFunc;
			}
			set
			{
				AssignPropertyValue(ref iconCharacterFunc, value, true);
			}
		}

		async Task RefreshIconCharacterAsync(TParameter parameter)
		{
			if (iconCharacterFunc != null)
			{
				IconCharacter = await iconCharacterFunc(parameter);
			}
		}

		string iconCharacter;

		public string IconCharacter
		{
			get
			{
				return iconCharacter;
			}
			set
			{
				AssignPropertyValue(ref iconCharacter, value, false);
			}
		}
		#endregion

		public override bool CanExecute(TParameter parameter)
		{
			bool result = base.CanExecute(parameter);

			RefreshUIProperties(parameter);

			return result;
		}

		public override async Task<bool> CanExecuteAsync(TParameter parameter)
		{
			bool result = await base.CanExecuteAsync(parameter);

			await RefreshUIPropertiesAsync(parameter);

			return result;
		}

		async void RefreshUIProperties(TParameter parameter)
		{
			try
			{
				await RefreshUIPropertiesAsync(parameter);
			}
			catch (Exception ex)
			{
				if (!HandleException(ex, this))
				{
					throw;
				}
			}
		}

		protected virtual async Task RefreshUIPropertiesAsync(TParameter parameter)
		{
			parameter = ProcessParameter(parameter);

			await RefreshVisibilityAsync(parameter);
			await RefreshTextAsync(parameter);
			await RefreshIconUrlAsync(parameter);
			await RefreshIsCheckedAsync(parameter);
			await RefreshIconCharacterAsync(parameter);
			await RefreshIconFontAsync(parameter);
		}

		async Task RefreshVisibilityAsync(TParameter parameter = default(TParameter))
		{
			parameter = ProcessParameter(parameter);

			if (isVisibleFunc != null)
			{
				Visible = await isVisibleFunc(parameter);
			}
		}

		async Task RefreshTextAsync(TParameter parameter = default(TParameter))
		{
			parameter = ProcessParameter(parameter);

			if (textFunc != null)
			{
				Text = await textFunc(parameter);
			}
		}

		public override async Task RefreshAsync(object commandParameter = null)
		{
			TParameter parameter = ProcessParameter(commandParameter);
			await base.RefreshAsync(parameter);
			await RefreshUIPropertiesAsync(parameter);
		}

	}

	public class UICommand : UICommand<object>
	{
		public UICommand(
			Action<object> executeAction, 
			Func<object, bool> canExecuteFunc = null,
			string id = null,
			IExceptionHandler exceptionHandler = null,
			[CallerFilePath] string filePath = null,
			[CallerLineNumber] int lineNumber = 0)
			: base(executeAction, canExecuteFunc, id,
					exceptionHandler, filePath, lineNumber)
		{
		}

		public UICommand(
			string id,
			Func<object, Task> executeAction,
			Func<object, Task<bool>> canExecuteFunc = null,
			IExceptionHandler exceptionHandler = null,
			[CallerFilePath] string filePath = null,
			[CallerLineNumber] int lineNumber = 0)
			: base(id, executeAction, canExecuteFunc,
					exceptionHandler, filePath, lineNumber)
		{
		}
	}

	public class UICompositeCommand : IUICommand, INotifyPropertyChanged
	{
		IUICommand selectedCommand;
		readonly List<IUICommand> commands = new List<IUICommand>();
		
		public List<IUICommand> Commands => commands;

		public string Id { get; set; }

		readonly Dictionary<IUICommand, object> commandParameters 
			= new Dictionary<IUICommand, object>();

		public void SetCommandParameter(IUICommand command, object parameter)
		{
			if (parameter != null)
			{
				commandParameters[command] = parameter;
			}
			else
			{
				commandParameters.Remove(command);
			}
		}

		public async Task RefreshAsync(object commandParameter = null)
		{
			foreach (IUICommand uiCommand in commands)
			{
				object parameter;
				commandParameters.TryGetValue(uiCommand, out parameter);

				await uiCommand.RefreshAsync(parameter ?? commandParameter);
			}
		}

		public IUICommand SelectedCommand
		{
			get
			{
				return selectedCommand;
			}
			set
			{
				ArgumentValidator.AssertNotNull(value, "value");

				if (value == selectedCommand)
				{
					return;
				}

				if (!commands.Contains(value))
				{
					throw new ArgumentOutOfRangeException("Command is not contained within the collection.");
				}

				if (selectedCommand != null)
				{
					StopMonitoringCommand(selectedCommand);
				}
				selectedCommand = value;
				MonitorCommand(selectedCommand);

				OnPropertyChanged(string.Empty);
			}
		}

		public int SelectedCommandIndex
		{
			get
			{
				if (selectedCommand == null)
				{
					return -1;
				}

				return commands.IndexOf(selectedCommand);
			}
			set
			{
				if (value < 0 || value > commands.Count - 1)
				{
					throw new ArgumentOutOfRangeException();
				}

				SelectedCommand = commands[value];
			}
		}

		void MonitorCommand(IUICommand uiCommand)
		{
			var inpc = uiCommand as INotifyPropertyChanged;
			if (inpc == null)
			{
				return;
			}

			inpc.PropertyChanged -= HandleSelectedCommandPropertyChanged;
			inpc.PropertyChanged += HandleSelectedCommandPropertyChanged;
		}

		void StopMonitoringCommand(IUICommand uiCommand)
		{
			var inpc = uiCommand as INotifyPropertyChanged;
			if (inpc == null)
			{
				return;
			}

			inpc.PropertyChanged -= HandleSelectedCommandPropertyChanged;
		}

		void HandleSelectedCommandPropertyChanged(object sender, PropertyChangedEventArgs e)
		{
			// ReSharper disable once ExplicitCallerInfoArgument
			OnPropertyChanged(e.PropertyName);
		}

		public UICompositeCommand(params IUICommand[] commands)
		{
			this.commands.AddRange(commands);

			if (commands.Any())
			{
				SelectedCommandIndex = 0;
			}
		}

		public bool CanExecuteDefaultValue { get; set; }

		public bool CanExecute(object parameter)
		{
			if (selectedCommand != null)
			{
				return selectedCommand.CanExecute(parameter);
			}

			return CanExecuteDefaultValue;
		}

		public void Execute(object parameter)
		{
			selectedCommand?.Execute(parameter);
		}

		public event EventHandler CanExecuteChanged;

		public string TextDefaultValue { get; set; }

		public string Text
		{
			get
			{
				if (selectedCommand != null)
				{
					return selectedCommand.Text;
				}

				return TextDefaultValue;
			}
		}

		bool visibleDefaultValue = true;

		public bool VisibleDefaultValue
		{
			get
			{
				return visibleDefaultValue;
			}
			set
			{
				visibleDefaultValue = value;
			}
		}

		public bool Visible
		{
			get
			{
				if (selectedCommand != null)
				{
					return selectedCommand.Visible;
				}

				return visibleDefaultValue;
			}
		}
		
		public bool Enabled
		{
			get
			{
				if (selectedCommand != null)
				{
					return selectedCommand.Enabled;
				}

				return true;
			}
		}

		public string IconUrl => selectedCommand?.IconUrl;

		public string IconCharacter => selectedCommand?.IconCharacter;

		public string IconFont => selectedCommand?.IconFont;

		public bool IsChecked
		{
			get
			{
				if (selectedCommand != null)
				{
					return selectedCommand.IsChecked;
				}

				return false;
			}
		}

		public event PropertyChangedEventHandler PropertyChanged;

		protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
		{
			PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
		}
	}
}
